package cn.wyz.basic.service.impl;

import cn.wyz.basic.constants.BaseConstants;
import cn.wyz.basic.dto.LoginDto;
import cn.wyz.basic.dto.RegisterDto;
import cn.wyz.basic.dto.WeChatDto;
import cn.wyz.basic.exception.BusinessException;
import cn.wyz.basic.jwt.JwtUtils;
import cn.wyz.basic.jwt.LoginResult;
import cn.wyz.basic.jwt.RsaUtils;
import cn.wyz.basic.service.ILoginService;
import cn.wyz.basic.util.AjaxResult;
import cn.wyz.basic.util.HttpClientUtils;
import cn.wyz.basic.util.MD5Utils;
import cn.wyz.basic.util.StrUtils;
import cn.wyz.system.domain.Menu;
import cn.wyz.system.mapper.MenuMapper;
import cn.wyz.system.mapper.PermissionMapper;
import cn.wyz.user.domain.Logininfo;
import cn.wyz.user.domain.User;
import cn.wyz.user.domain.Wxuser;
import cn.wyz.user.mapper.LogininfoMapper;
import cn.wyz.user.mapper.UserMapper;
import cn.wyz.user.mapper.WxuserMapper;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.security.PrivateKey;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 登录的服务层
 * 6.23:增加菜单权限,存在jwt中,替换token,避免每次登陆都要查权限
 * 并且原版token是放在本机redis中的,现在要换为JWT交给访问的浏览器端,不存在我们这边,进行改造
 * JWT加密顺序与解密顺序是对称的:--公私不影响使用,只是表明两把对称的密钥
 *  1.对方的公钥1次加密,自己的私钥2次加密
 *  2.解密时,先用对方的私钥解密1次,再用自己的公钥2次解密
 *
 */
@Service
public class LoginServiceImpl implements ILoginService {
    @Autowired
    private LogininfoMapper logininfoMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private WxuserMapper wxuserMapper;

    @Autowired
    private MenuMapper menuMapper;
    @Autowired
    private PermissionMapper permissionMapper;

    @Autowired
    private UserMapper userMapper;

    @Override
    public Map<String, Object> account(LoginDto dto) {
        String username = dto.getUsername();
        String password = dto.getPassword();
        Integer type = dto.getType();
        //1.校验,三个空校验
        if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password) || StringUtils.isEmpty(type)){
            throw new BusinessException("参数必填！！");
        }
        //2.根据username + type 查询logininfo
        // 要查询username phone email三个字段,这里不用筛查到底输入的是什么,只用在sql中满足一个用户名匹配三类型即可
        // type是为了筛选出在职员工,才让登录,直接查中间表t_Logininfo
        Logininfo logininfo = logininfoMapper.selectByUsernameAndType(username,type);
            //2.1如果用户信息没有查到
        if(logininfo == null){
            throw new BusinessException("账户不存在！！！");
        }
            //2.2查到了校验密码是否一致,前端传回来的密码要用salt加密才能与数据库中的对比
        String salt = logininfo.getSalt();
        String md5password = MD5Utils.encrypByMd5(salt+password);
        if(!md5password.equals(logininfo.getPassword())){
            //3.密码错误，报错
            throw new BusinessException("密码错误！！！");
        }
        //进入登陆成功的方法
        return loginSuccessHandler(logininfo);
        /*//4.通过表示登录成功,使用UUid工具类在后端搞一个uuid,当作唯一的key:token,存在redis,值就是logininfo
        String token = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(
                token,
                logininfo,//存对象到redis中,要序列化为二进制,要在domain类中实现 Serializable
                30,
                TimeUnit.MINUTES
        );
        //5.返回这个redis给前端,因为涉及了拦截器,如果没有token那么会被弹回登录页
        Map<String, Object> map = new HashMap<>();
        map.put("token",token);
            //5.1这是返回给前端的map,装在请求体里面,如果被拦截那么密码就会暴露,所以这里要修改掉再传
        logininfo.setSalt(null);
        logininfo.setPassword(null);
        map.put("logininfo",logininfo);
        return map;*/
    }

    /**
     *  实现登录成功以后,要把数据与权限/菜单用JWT规范传走
     */
    public Map<String,Object> loginSuccessHandler(Logininfo logininfo) {
        //JWT中的载荷数据类型我们封装了一个loginresult对象来包装数据
        LoginResult loginResult = new LoginResult();
        loginResult.setLogininfo(logininfo);
        //1.判断是否是员工吗，如果是员工，才需要查询权限
        if(logininfo.getType() == 0){//员工
            //我们需要查询出当前登录人对应的菜单+按钮权限
            // 菜单权限
            List<Menu> menus = menuMapper.loadMenusByLogininfoId(logininfo.getId());
            // 按钮权限
            List<String> permissions = permissionMapper.loadPermissionsByLogininfoId(logininfo.getId());
            loginResult.setMenus(menus);
            loginResult.setPermissions(permissions);
        }
        // 把登录人信息 + 权限 加密封装成JWT串，返回给前端 || 已经获取到了以后,用私钥进行加密,因为我们本机随便玩玩,所以就少了一步获取对方公钥的操作
        PrivateKey privateKey = RsaUtils.getPrivateKey(JwtUtils.class.getClassLoader().getResource("hrm_auth_rsa").getFile());
        //使用私钥将loginResult 加密成JWT串
        String jwtStr = JwtUtils.generateTokenExpireInMinutes(loginResult, privateKey, 30);
        Map<String, Object> map = new HashMap<>();
        map.put("token",jwtStr);// 此时的token是jwt串
        //我们是高级技师，应该考虑到一些敏感信息不要返回给前端
        logininfo.setSalt(null);
        logininfo.setPassword(null);
        map.put("logininfo",logininfo);
        map.put("menus",loginResult.getMenus());
        map.put("permissions",loginResult.getPermissions());
        return map;

    }

    /**
     * 微信扫码登录第二部分:
     * 前端携带微信回调页面的code码,后端通过code像微信申请到临时token与openId,去微信访问资源
     * @param dto
     */
    @Override
    public AjaxResult wechat(WeChatDto dto) {
        String code = dto.getCode();
        if(StringUtils.isEmpty(code)){
            throw new BusinessException("code不能为空！！");
        }

        //1.1通过code从微信获取token+openid,要手动发送下面这个地址并拼接code进行请求
        //https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
        String tokenUrl = BaseConstants.weChatConstants.TOKEN_URL//因为地址是死的常量,所以抽出来
                .replace("APPID", BaseConstants.weChatConstants.APPID)
                .replace("SECRET", BaseConstants.weChatConstants.SECRET)
                .replace("CODE", code);
        String tokenStr = HttpClientUtils.httpGet(tokenUrl);
        //tokenStr:"https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID";
        //1.2申请回来的地址是拼接在地址栏上的,所以要转为json格式传回前端
        JSONObject tokenObj = JSONObject.parseObject(tokenStr);
        //1.3json对象,也是用key取value,只是看取的类型,单独把access_token与openid拿到
        String accessToken = tokenObj.getString("access_token");
        String openId = tokenObj.getString("openid");

        //2.1通过openid查询t_wxuser表，判断是否wxuser存在并且user_id不为空
        Wxuser wxuser = wxuserMapper.selectByOpenId(openId);
        //2.2通过openID判断用户是否注册过.     账户存在,但未跟我们的帐号绑定user_id为空,跳转绑定;账户存在,也绑定我们的账号,直接免密登录
        if(wxuser != null && wxuser.getUserId() != null){
            Logininfo logininfo = logininfoMapper.selectByUserId(wxuser.getUserId());
            /*3.储存redis
            String token = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(
                    token,
                    logininfo,//存对象到redis中,要序列化为二进制,要在domain类中实现 Serializable
                    30,
                    TimeUnit.MINUTES
            );
            //5.返回这个redis给前端,因为涉及了拦截器,如果没有token那么会被弹回登录页
            Map<String, Object> map = new HashMap<>();
            map.put("token",token);
            //5.1这是返回给前端的map,装在请求体里面,如果被拦截那么密码就会暴露,所以这里要修改掉再传
            logininfo.setSalt(null);
            logininfo.setPassword(null);
            map.put("logininfo",logininfo);
            return AjaxResult.make().setResultObj(map);*/
            return AjaxResult.make().setResultObj(loginSuccessHandler(logininfo));
        }
        //2.3 账户存在,但未跟我们的帐号绑定user_id为空,跳转绑定,还是要传回access_token与openid,只能自己拼好
        String bindParam = "?access_token="+accessToken+"&openid="+openId;
        //这里返回的结果类型不一致,所以直接包装在ajaxresult里面返回
        return AjaxResult.make().setSuccess(false).setResultObj(bindParam);
    }

    /**
     * 微信扫码绑定实现
     * @param dto
     * @return
     */
    @Override
    public AjaxResult binder(WeChatDto dto) {
        //1.根据phone 查询t_user是否曾今已经存在,
        User user = userMapper.selectByPhone(dto.getPhone());
        //如果没查到,就走注册流程,先准备一个空的logininfo,要级联操作
        Logininfo logininfo = null;
        if(user != null){
            logininfo = logininfoMapper.selectByUserId(user.getId());
        } else {
            //生成一个新的User与logininfo
            user = dto2User(dto);
            logininfo =user2Logininfo(user);
            logininfoMapper.insert(logininfo);
            user.setLogininfoId(logininfo.getId());
            userMapper.insert(user);
        }
        //2.根据token + openid 去微信查询微信的用户基本信息
        //tokenStr:"https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID";
        String wxUserUrl = BaseConstants.weChatConstants.WXUSER_URL
                .replace("ACCESS_TOKEN", dto.getAccessToken())
                .replace("OPENID", dto.getOpenId());
        String wxUserStr = HttpClientUtils.httpGet(wxUserUrl);
        //2.1根据拿到的基本信息,生成t_wxuser对象
        Wxuser wxuser = JSONObject.parseObject(wxUserStr, Wxuser.class);
        //wxUser与User关联的字段就是userId,要级联操作
        wxuser.setUserId(user.getId());
        wxuserMapper.insert(wxuser);
        /*//2.2直接免密登录
        String token = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(
                token,
                logininfo,//存对象到redis中,要序列化为二进制,要在domain类中实现 Serializable
                30,
                TimeUnit.MINUTES
        );
        //5.返回这个redis给前端,因为涉及了拦截器,如果没有token那么会被弹回登录页
        Map<String, Object> map = new HashMap<>();
        map.put("token",token);
        //5.1这是返回给前端的map,装在请求体里面,如果被拦截那么密码就会暴露,所以这里要修改掉再传
        logininfo.setSalt(null);
        logininfo.setPassword(null);
        map.put("logininfo",logininfo);
        return AjaxResult.make().setResultObj(map);*/
        return AjaxResult.make().setResultObj(loginSuccessHandler(logininfo));
    }




    /**
     * 级联操作-生成user帐号
     * @param dto 用传回来的绑定weChatDto
     * @return
     */
    private User dto2User(WeChatDto dto) {
        User user = new User();
        //3.手机号注册,就用手机号做用户名
        user.setUsername(dto.getPhone());
        user.setPhone(dto.getPhone());
        //1要手动设置盐值
        String salt = StrUtils.getComplexRandomString(32);
        user.setSalt(salt);
        //2对密码加密以后再存进user
        String password = MD5Utils.encrypByMd5(salt + dto.getPassword());
        user.setPassword(password);
        return user;
    }

    /**
     * 级联操作,生成logininfo对象
     * @param user
     * @return
     */
    private Logininfo user2Logininfo(User user) {
        Logininfo logininfo = new Logininfo();
        BeanUtils.copyProperties(user,logininfo);
        //手动对t_logininfo表中的type(权限)设置,设置用户,不能直接给默认值
        logininfo.setType(1);
        return logininfo;
    }
}
